<?php if (!defined('CORE_DIR')) die('Wrong entry point!');

/* 
 * Copyright 2010 Anton Muraviev a.k.a kuromaisu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */

Aero::define('AeroPipeline', AS_CLASS);

/* -----------------------------------------------------------------------------
 * Class: Pipeline
 * -----------------------------------------------------------------------------
 */
class AeroPipeline
{
	protected $names = array();
	protected $segments = array();
	protected $contexts = array();
	protected $context_cache = array();
	
	public function __construct($pipeline = '')
	{
		$this->resetContext();
		$this->set($pipeline);
	}
	
	/*
	 * Method: addContext
	 * 
	 * Pass NULL to use global functions.
	 */
	public function addContext($contexts)
	{
		if (!is_array($contexts))
		{
			$contexts = array($contexts);
		}
		else
		{
			$contexts = array_reverse($contexts);
		}
		
		foreach ($contexts as $context)
		{
			if (is_object($context) || is_string($context) && class_exists($context) || is_null($context))
			{
				array_unshift($this->contexts, $context);
			}
		}
	}
	
	/*
	 * Method: setContext
	 */
	public function setContext($context)
	{
		$this->contexts = array();
		$this->addContext($context);
	}
	
	/*
	 * Method: resetContext
	 */
	public function resetContext()
	{
		$this->contexts = array();
		$this->context_cache = array();
	}
	
	#
	# Method: test
	# 
	# Runs a value or an array of values through pipe until callback returns false
	# or while segment functions return the same value as $test variable.
	# 
	# Callback function should except one parameter and return TRUE or FALSE.
	# 
	# Parameters:
	# * misc -- scalar value or an array of scalar values to test.
	# * misc -- a callback function or any value to test against.
	# 
	# Returns:
	# * `true`, if all segments succeeded.
	# * Segment in `array` form if this segment failed.
	#
	public function test($value, $test)
	{
		if (is_array($value))
		{
			$return = array();
			
			foreach ($value as $key => $_value)
			{
				$return[$key] = $this->test($_value,$test);
			}
			
			return $return;
		}
		
		foreach ($this->segments as $attributes)
		{
			$func = array_shift($attributes);
			$callback = $this->_getCallback($func);
			
			// Exit if segment cannot be mapped
			if (!$callback)
			{
				trigger_error('Pipe segment <code>'.$func.'[]</code> could not be applied, because no such function was found.', E_USER_NOTICE);
				return array_merge(array($func), $attributes);
			}
			
			if (false !== ($key = array_search('?', $attributes)))
			{
				$attributes[$key] = $value;
			}
			else
			{
				array_unshift($attributes,$value);
			}
			
			// Call the segment
			$_value = call_user_func_array($callback, $attributes);
			
			// Test the returned value now.
			if (is_callable($test))
			{
				$_test = call_user_func($test,$_value);
			}
			else
			{
				$_test = ($_value == $test);
			}
			
			if ($_test !== true)
			{
				return array_merge(array($func), $attributes);
			}
		}
		
		return true;
	}
	
	/*
	 * Method: modify
	 * 
	 * Runs a value or an array of values through the pipe.
	 */
	public function modify($value)
	{
		if (is_array($value))
		{
			return array_map(array($this,'modify'), $value);
		}
		
		foreach ($this->segments as $attributes)
		{
			$func = array_shift($attributes);
			$callback = $this->_getCallback($func);
			
			// Exit if segment cannot be mapped
			if (!$callback)
			{
				trigger_error('Pipe segment <code>'.$func.'[]</code> could not be applied, because no such function was found.', E_USER_NOTICE);
				return $value;
			}
			
			if (false !== ($key = array_search('?', $attributes)))
			{
				$attributes[$key] = $value;
			}
			else
			{
				array_unshift($attributes,$value);
			}
			
			$value = call_user_func_array($callback, $attributes);
		}
		
		return $value;
	}
	
	/*
	 * Method: toArray
	 * 
	 * Returns pipe in an array form.
	 * 
	 * Returns:
	 * 	array
	 */
	public function toArray()
	{
		return $this->segments;
	}
	
	/*
	 * Method: toArray
	 * 
	 * Returns pipe in a string form.
	 * 
	 * Returns:
	 * 	string
	 */
	public function toString()
	{
		$pipe = array();
		
		foreach ($this->segments as $attributes)
		{
			$segment = array_shift($attributes);
			
			if (!empty($attributes))
			{
				$pipe[] = $segment . '['.implode(',', 
					array_map(array($this,'_spelloutAttribute'), $attributes)
				).']';
			}
			else
			{
				$pipe[] = $segment;
			}
		}
		
		return implode('|',$pipe);
	}
	
	/*
	 * Method: set
	 * 
	 * Parameters:
	 * 	misc	-	pipeline as an array or string
	 */
	public function set($pipeline)
	{
		$this->segments = array();
		
		if (is_string($pipeline))
		{
			$this->_fromString($pipeline);
		}
		else if (is_array($pipeline))
		{
			$this->_fromArray($pipeline);
		}
		
	}
	
	/*
	 * Method: add
	 * 
	 * Adds a new segment to the pipeline.
	 * 
	 * Parameters:
	 * 	misc	-	pipeline segment as an array or string
	 * 	int		-	offset to insert this segment at. If negative
	 * 				it counted from the end.
	 * 
	 */
	public function add($segment, $position = -1)
	{
		$segment = $this->_parseSegment($segment);
		$name = $segment[0];
		
		if ($position < 0)
		{
			$length = $position;
			$position+= count($this->segments) + 1;
		}
		else
		{
			$length = 0;
		}
		
		$this->names[$name] = empty($this->names[$name]) ? 1 : $this->names[$name] + 1;
		
		array_splice($this->segments, $position, $length, array($segment));
	}
	
	/*
	 * Method: append
	 * 
	 * Parameters:
	 * 	misc	-	pipeline segment as an array or string
	 */
	public function append($segment)
	{
		$this->add($segment, -1);
	}
	
	/*
	 * Method: prepend
	 * 
	 * Parameters:
	 * 	misc	-	pipeline segment as an array or string
	 */
	public function prepend($segment)
	{
		$this->add($segment, 0);
	}
	
	/*
	 * Method: has
	 * 
	 * Parameters:
	 * 	string	-	segment name
	 * 
	 * Returns:
	 * 	TRUE if a segment with this name exists or FALSE
	 */
	public function has($name)
	{
		return !empty($this->names[$name]);
	}
	
	/*
	 * Method: delete
	 * 
	 * Deletes all segments with this name.
	 * 
	 * Parameters:
	 * 	string	-	segment name
	 */
	public function delete($name)
	{
		$this->names[$name] = 0;

		foreach ($this->segments as $i => $segment)
		{
			if ($segment[0] === $name)
			{
				unset($this->segments[$i]);
			}
		}
	}
	
	/*
	 * Method: _getCallback
	 * 
	 * Returns correct callback for such a function name by looking for it through all contexts.
	 * 
	 * Parameters:
	 * 	string	-	function name
	 * 
	 * Returns:
	 * 	Array or string.
	 */
	protected function _getCallback($func)
	{
		$alt = str_replace('_', '', $func);
		
		if (isset($this->context_cache[$func]))
		{
			return $this->context_cache[$func];
		}
		
		foreach ($this->contexts as $context)
		{        
			if (is_null($context) && function_exists($func))
			{
				$this->context_cache[$func] = $func;
				return $func;
			}             
			else if (method_exists($context,$func))
			{
				$callback = array($context, $func);
				$this->context_cache[$func] = $callback;
				return $callback;
			}
			else if (method_exists($context,$alt))
			{
				$callback = array($context, $alt);
				$this->context_cache[$func] = $callback;
				return $callback;
			}
		}
	}
	
	/*
	 * Method: _fromString
	 * 
	 * Sets pipeline by parsing a string.
	 * 
	 * Parameters:
	 * 	string	-	pipeline
	 */
	protected function _fromString($string)
	{
		$segments = explode('|',$string);
		
		foreach ($segments as $segment)
		{
			$segment = trim($segment);
			
			if (!$segment)
			{
				continue;
			}
			
			$this->segments[] = $this->_parseSegment($segment);
		}
		
	}
	
	/*
	 * Method: _fromArray
	 * 
	 * Sets pipe from an array that loosely follows the inner format.
	 * 
	 * Parameters:
	 * 	string	-	array
	 */
	protected function _fromArray($array)
	{
		foreach ($array as $key => $value)
		{
			if (is_array($value))
			{
				if (is_string($key))
				{
					array_unshift($value,$key);
				}
				
				// Make sure all segment elements are scalar
				foreach ($value as $item)
				{
					if (!is_scalar($item))
					{
						throw new ErrorException('Pipe segment attributes must be scalar: <code>'.print_r($value,true).'</code>', 0, E_USER_ERROR);
					}
				}
				
				$this->segments[] = $value;
			}
			elseif (is_string($value))
			{
				$this->segments[] = array($value);
			}
		}
	}
	
	/*
	 * Method: _parseSegment
	 * 
	 * Parses segment as string or array and returns it in a standard array form.
	 * 
	 * Parameters:
	 * 	array || string	-	segment to parse
	 * 
	 * Returns:
	 * 	array
	 */
	protected function _parseSegment($segment)
	{
		// If segment has attributes...
		if (preg_match('~(\w+)\s*\[?([^\]]*)\]~', $segment, $matches))
		{
			// ...parse them...
			preg_match_all('~(["\'])?((?(1).+?|[^,]+))(?<!\\\\)(?(1)\\1)~', trim($matches[2]), $attributes);
			// ...and return as one linear array.
			return array_merge(array($matches[1]), str_replace('\"', '"', $attributes[2]));
		}
		else
		{
			return array($segment);
		}
		
	}
	
	/*
	 * Method: _spelloutAttribute
	 * 
	 * Escapes commas inside attributes.
	 * 
	 * Parameters:
	 * 	string	-	attribute to parse
	 * 
	 * Returns:
	 * 	string
	 */
	protected function _spelloutAttribute($attribute)
	{
		if (strpos($attribute, ',') !== false)
		{
			return '"'.str_replace('"','\"',$attribute).'"';
		}
		
		return $attribute;
	}
}